﻿using System;
using System.Collections.ObjectModel;
using System.Net;
using System.Windows;
using BingleMaps.Extensions;
using BingleMaps.Models;
using DropNet;
using DropNet.Exceptions;
using DropNet.Models;
using RestSharp;
using Serialization;

namespace BingleMaps.Services
{
	public static class DropBoxServices
	{
		private static DropNetClient _client;
		private static readonly string _apiKey = Application.Current.Resources["DropBoxAppKey"].ToString();
		private static readonly string _appSecret = Application.Current.Resources["DropBoxAppSecret"].ToString();

		public static void CreateAccount(string email, string firstName, string lastName, string password, Action<RestResponse> success, Action<DropboxException> fail)
		{
			if (_client == null)
			{
				_client = new DropNetClient(_apiKey, _appSecret, string.Empty, string.Empty);
			}
			_client.CreateAccountAsync(email, firstName, lastName, password, success, fail);
		}

		public static void Login(string email, string password, Action<UserLogin> success, Action<DropboxException> fail)
		{
			if (_client == null)
			{
				_client = new DropNetClient(_apiKey, _appSecret, string.Empty, string.Empty);
			}
			_client.LoginAsync(email, password, success, fail);
		}

		private static void Validate(UserLogin login)
		{
			if (login == null)
			{
				throw new ArgumentException(@"login cannot be null.", "login");
			}
			if (_client == null)
			{
				_client = new DropNetClient(_apiKey, _appSecret, login.Token, login.Secret);
			}
		}

		private static void Upload(UserLogin login, ObservableCollection<UserPin> userPins, Action<RestResponse> success, Action<DropboxException> fail)
		{
			Validate(login);
			_client.UploadFileAsync("/", "BingleMaps.dat", SilverlightSerializer.Serialize(userPins), success, fail);
		}

		private static void Download(UserLogin login, Action<RestResponse> success, Action<DropboxException> fail)
		{
			Validate(login);
			_client.GetFileAsync("/BingleMaps.dat", success, fail);
		}

		private static void SuccessCallback(ref UserPinLog log, RestResponse response, Action<RestResponse> callback)
		{
			log.Changes.Clear();
			callback(response);
		}

		public static void Sync(UserLogin login, UserPinLog log, Action<RestResponse> success, Action<DropboxException> fail)
		{
			// Download data from server first.
			Download(login, successRsp =>
			{
				// Check if download status.
				// If download is successful, try deserialize the data.
				ObservableCollection<UserPin> onlinePins;
				try
				{
					onlinePins = (ObservableCollection<UserPin>)SilverlightSerializer.Deserialize(successRsp.RawBytes);
				}
				catch
				{
					// If serialization fails, then the online data file might be corrupted.
					// In this case, re-upload local pins to overwrite the online data file.
					Upload(login, log.Pins, success, fail);
					return;
				}
				if (onlinePins.Count == 0)
				{
					// If online doesn't have pins, just upload local.
					Upload(login, log.Pins, success, fail);
				}
				else if (log.Pins.Count == 0 && log.Changes.Count == 0)
				{
					// If local doesn't have pins, just replace with online.
					log.Pins = onlinePins;
					success(successRsp);
				}
				else
				{
					// Worst case, do delta checks and update accordingly.
					UserPin tempPin;
					UserPinChange change;

					// Process each online pin.
					foreach (var onlinePin in onlinePins)
					{
						// Check if the local pin list contains the current online pin.
						tempPin = log.GetPin(onlinePin.Key);
						if (tempPin != null)
						{
							continue;
						}
						// If local doesn't have this online pin, check if this pin has been deleted locally.
						change = log.GetChange(onlinePin.Key);
						if (change == null)
						{
							// If the pin is not in the chagne list, then this pin is added by another source and synced to the online place.
							// Add this pin to the local list, without logging it into the change list.
							onlinePin.IsEditing = false;
							log.Pins.Add(onlinePin);
						}
						else if (change.Action == UserPinAction.Delete)
						{
							// If the pin is in the chagne list, and the pin is marked for deletion,
							// ignore adding this pin as it's supposed to be deleted.
							continue;
						}
						else
						{
							// If none of the above happens, then there's something wrong, throw the exception.
							throw new InvalidOperationException("There shouldn't be the case that an online user pin is missing from local list and the registered local change action for this pin is not deletion.");
						}
					}

					var removeList = new Collection<UserPin>();
					// Process each local pin.
					foreach (var localPin in log.Pins)
					{
						// Check if the online pin list contains the current local pin.
						tempPin = onlinePins.GetUserPin(localPin.Key);
						if (tempPin != null)
						{
							continue;
						}
						// If online doesn't have this local pin, check if this pin is newly added.
						change = log.GetChange(localPin.Key);
						if (change == null)
						{
							// If this local pin is not in the change list, then this pin is removed by another source and synced to the online place.
							// Add it to the pending remove list.
							removeList.Add(localPin);
						}
						else if (change.Action == UserPinAction.Add)
						{
							// If the pin is in the change list, and the pin is marked as newly added,
							// ignore removing this pin as it's supposed to be added.
							continue;
						}
						else
						{
							// If none of the above happens, then there's somehting wrong, throw the exception.
							throw new InvalidOperationException("There shouldn't be the case that an local user pin is missing from the online list and the registered local change action is not addition.");
						}
					}

					// Remove pins from the pending removal list.
					foreach (var localPin in removeList)
					{
						log.Pins.Remove(localPin);
					}

					// Clear pending removal list.
					removeList.Clear();

					// We are done syncing the pins locally, upload them now.
					Upload(login, log.Pins, rsp => SuccessCallback(ref log, rsp, success), fail);
				}
			},
			failRsp =>
			{
				if (failRsp.StatusCode == HttpStatusCode.NotFound)
				{
					// If there is no online data, just upload local.
					Upload(login, log.Pins, success, fail);
				}
				else
				{
					fail.ExecuteIfNotNull(failRsp);
				}
			});
		}
	}
}
